/*
 * The MIT License
 *
 * Copyright 2014 David Rodenas Pico.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package edu.uoc.sd.fr14.impl;

import static edu.uoc.sd.fr14.test.ServerStateCreator.CUSTOMER1000;
import static edu.uoc.sd.fr14.test.ServerStateCreator.FLIGHT_EMPTY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import edu.uoc.sd.fr14.model.Nseq;
import edu.uoc.sd.fr14.model.OpId;
import edu.uoc.sd.fr14.model.ServerId;
import edu.uoc.sd.fr14.model.SubLog;

/**
 * Test the server implementation assuming replica and leader with bully.
 * Fixed leader with no changes.
 * Requires update replicas state and forward to leader requests.
 * Requires to deal with timeouts and recover petitions.
 * Requires to recover from leader failures and repetitions.
 *
 * @author drodenas
 */
public class ServerImplPh4 extends ServerImplCommon {
    
    public ServerImplPh4() {
    }
    
    @BeforeClass
    public static void setUpClass() {
    }
    
    @AfterClass
    public static void tearDownClass() {
    }

    @Before
    @Override
    public void setUp() {
        super.setUp();
    }
    
    @After
    @Override
    public void tearDown() {
        super.tearDown();
    }

    
    /* *********************************************************************
     * *
     * * ABORT
     * *
     * *********************************************************************/
    
    @Test
    public void abortStepsDown() {
        makeMeLeader();
        
        /////////////////////////////////////
        server.abort();
        /////////////////////////////////////

        assertFalse("I should not be leader", serverState.getRole().isLeader());
        testCalls.check();        
    }
    
    @Test 
    public void abortDoesNotChangesFollowerLowerLeader() {
        makeMeFollower(LOWER);

        /////////////////////////////////////
        server.abort();
        /////////////////////////////////////

        assertEquals("Leader should remain the same", LOWER, serverState.getRole().getLeader());
        testCalls.check();        
    }

    
    @Test 
    public void abortDoesNotChangesFollowerGreaterLeader() {
        makeMeFollower(GREATER);

        /////////////////////////////////////
        server.abort();
        /////////////////////////////////////

        assertEquals("Leader should remain the same", GREATER, serverState.getRole().getLeader());
        testCalls.check();        
    }
    
    @Test
    public void abortCancelElections() {
        serverState.getRole().beginElections();
        
        /////////////////////////////////////
        server.abort();
        /////////////////////////////////////

        assertFalse("I should not be leader", serverState.getRole().isDoingElections());
        testCalls.check();        
    }
    
    @Test
    public void abortLeaderCancelElections() {
        makeMeLeader();
        abortCancelElections();
    }
    
    @Test
    public void abortCancelOps() {
        serverState.getLog().annotate(FLIGHT_EMPTY, CUSTOMER1000, CUSTOMER1000);
        
        /////////////////////////////////////
        server.abort();
        /////////////////////////////////////

        assertFalse("There should be no waiting ops", serverState.getLog().hasWaitingOps());
        testCalls.check();        
    }

    
    /* *********************************************************************
     * *
     * * ELECT
     * *
     * *********************************************************************/
    
    @Test
    public void electLostSetsElectionTo() {
        
        cluster.clock().setElectionTo_expects(ME);
        /////////////////////////////////////
        server.elect(LOWER);
        /////////////////////////////////////

        assertTrue("clock.setElectionTo() should be called", cluster.clock().setElection_called);
    }
    
    @Test
    public void electFollowerOfTheSameSetsElectionTo() {
        makeMeFollower(LOWER);
        cluster.clock().setElectionTo_expects(ME);
        /////////////////////////////////////
        server.elect(LOWER);
        /////////////////////////////////////

        assertTrue("clock.setElectionTo() should be called", cluster.clock().setElection_called);
    }
    
    @Test
    public void electLostRespondsAbort() {
        makeMeLost();
        
        cluster.server(LOWER);
        /////////////////////////////////////
        server.elect(LOWER);
        /////////////////////////////////////
        
        assertTrue("abort() message should be sent", cluster.server(LOWER).abort_called);
    }
    
    @Test
    public void electFollowerRespondsAbort() {
        makeMeFollower();
        
        cluster.server(LOWER);
        /////////////////////////////////////
        server.elect(LOWER);
        /////////////////////////////////////

        assertTrue("abort() message should be sent", cluster.server(LOWER).abort_called);
    }
    
    
    /* *********************************************************************
     * *
     * * PREPARE
     * *
     * *********************************************************************/

    @Test
    public void prepareLeaderRespondsAbort() {
        makeMeLeader();
        
        OpId op = lowerState.getLog().annotate(FLIGHT_EMPTY, CUSTOMER1000, CUSTOMER1000);
        cluster.server(LOWER);
        /////////////////////////////////////
        server.prepare(op, LOWER);
        /////////////////////////////////////

        assertTrue("abort() message should be sent", cluster.server(LOWER).abort_called);
    }
    
    @Test
    public void prepareLostSetLeader() {
        makeMeLost();
        
        OpId op = lowerState.getLog().annotate(FLIGHT_EMPTY, CUSTOMER1000, CUSTOMER1000);
        /////////////////////////////////////
        server.prepare(op, LOWER);
        /////////////////////////////////////

        assertEquals("Leader should be the one seen", LOWER, serverState.getRole().getLeader());
        testCalls.check();
    }
    
    @Test
    public void prepareFollowerRespondsPrepared() {
        makeMeFollower(OTHER);
        
        OpId op = lowerState.getLog().annotate(FLIGHT_EMPTY, CUSTOMER1000, CUSTOMER1000);
        cluster.server(OTHER).prepared_expects(op);
        /////////////////////////////////////
        server.prepare(op, OTHER);
        /////////////////////////////////////

        assertTrue("prepared() message should be sent", cluster.server(OTHER).prepared_called);
    }

    
    /* *********************************************************************
     * *
     * * RECOVER
     * *
     * *********************************************************************/
    
    @Test
    public void recoverFromLeaderRespondsApply() {
        
        Nseq nseq = new Nseq(0);
        cluster.server(OTHER).apply_expects(nseq, ME);
        /////////////////////////////////////
        server.recover(nseq, true, OTHER);
        /////////////////////////////////////

        assertTrue("apply() message should be sent", cluster.server(OTHER).apply_called);
    }
    
    @Test
    public void recoverFromFollowerRespondsApply() {
        
        Nseq nseq = new Nseq(0);
        cluster.server(OTHER).apply_expects(nseq, ME);
        /////////////////////////////////////
        server.recover(nseq, false, OTHER);
        /////////////////////////////////////

        assertTrue("apply() message should be sent", cluster.server(OTHER).apply_called);
    }
    
    @Test
    public void recoverLostFromLeaderUpdatesLeader() {
        makeMeLost();
        
        Nseq nseq = new Nseq(0);
        /////////////////////////////////////
        server.recover(nseq, true, OTHER);
        /////////////////////////////////////

        assertEquals("leader should be the recovered one", OTHER, serverState.getRole().getLeader());
    }
    
    @Test
    public void recoverFollowerFromLeaderUpdatesLeader() {
        makeMeFollower(LOWER);
        
        Nseq nseq = new Nseq(0);
        /////////////////////////////////////
        server.recover(nseq, true, OTHER);
        /////////////////////////////////////

        assertEquals("leader should be the recovered one", OTHER, serverState.getRole().getLeader());
    }
            
    
    /* *********************************************************************
     * *
     * * TIMEOUT ELECTION BEGIN
     * *
     * *********************************************************************/
    
    @Test
    public void timeoutElectionBeginLostBeginElections() {
        makeMeLost();
        
        /////////////////////////////////////
        server.timeoutElectionBegin();
        /////////////////////////////////////

        assertTrue("elections should begun", serverState.getRole().isDoingElections());
    }
    
    @Test
    public void timeoutElectionBeginFollowerBeginElections() {
        makeMeFollower(GREATER);
        
        /////////////////////////////////////
        server.timeoutElectionBegin();
        /////////////////////////////////////

        assertTrue("elections should begun", serverState.getRole().isDoingElections());
    }
    
    @Test
    public void timeoutElectionBeginSendsElectToGreaters() {
        
        cluster.server(OTHER).elect_expects(ME);
        cluster.server(GREATER).elect_expects(ME);
        /////////////////////////////////////
        server.timeoutElectionBegin();
        /////////////////////////////////////

        assertTrue("elect() message should be sent", cluster.server(OTHER).elect_called);
        assertTrue("elect() message should be sent", cluster.server(GREATER).elect_called);
    }
    
    
    /* *********************************************************************
     * *
     * * TIMEOUT ELECTION END
     * *
     * *********************************************************************/

    @Test
    public void timeoutElectionEndDoNotBecomeLeaderIfNoElections() {
        
        /////////////////////////////////////
        server.timeoutElectionEnd();
        /////////////////////////////////////
        
        assertFalse("should not be leader", serverState.getRole().isLeader());
    }
    
    @Test
    public void timeoutElectionEndBecomeLeaderIfElections() {
        
        serverState.getRole().beginElections();
        /////////////////////////////////////
        server.timeoutElectionEnd();
        /////////////////////////////////////
        
        assertTrue("should be leader", serverState.getRole().isLeader());
        testCalls.check();
    }
    
    @Test
    public void timeoutElectionEndSendRecovers() {
        
        serverState.getRole().beginElections();
        cluster.server(LOWER).recover_expects(serverState.getNseqTracker().getCurrent(LOWER), true, ME);
        cluster.server(GREATER).recover_expects(serverState.getNseqTracker().getCurrent(GREATER), true, ME);
        /////////////////////////////////////
        server.timeoutElectionEnd();
        /////////////////////////////////////
        
        assertTrue("recover() message should be sent", cluster.server(LOWER).recover_called);
        assertTrue("recover() message should be sent", cluster.server(GREATER).recover_called);
    }
    
    @Test
    public void timeoutElectionsEndStopElections() {
        
        serverState.getRole().beginElections();
        /////////////////////////////////////
        server.timeoutElectionEnd();
        /////////////////////////////////////
        
        assertFalse("elections should be stopped", serverState.getRole().isDoingElections());
    }
    
    
    /* *********************************************************************
     * *
     * * SYNCHRONIZE
     * *
     * *********************************************************************/
    
    public void synchronzeSendCorrectNseqs(boolean isLeader) {

        for (int i = 51; i < 79; i++) if ((i % 11) % 3 != 0) {
            ServerId other = new ServerId(i);
            SubLog subLog = otherState.getLog().recover(new Nseq(i));
            serverState.getNseqTracker().advance(subLog, other);
            Nseq expectedNseq = serverState.getNseqTracker().getCurrent(other);
            
            cluster.server(other).recover_expects(expectedNseq, isLeader, ME);
        }
        
        /////////////////////////////////////
        server.synchronize();
        /////////////////////////////////////

        // check results
        for (ServerId other : cluster.getServers()) {
            assertTrue("recover() message should be sent", cluster.server(other).recover_called);
        }
    }
    
    @Test
    public void synchronzeLeaderSendCorrectNseqs() {
        makeMeLeader();
        synchronzeSendCorrectNseqs(true);
    }
    
    @Test
    public void synchronzeFollowerSendCorrectNseqs() {
        makeMeFollower(OTHER);
        synchronzeSendCorrectNseqs(false);
    }

}
